/*
 *双向绑定原理
 * 页面元素的值发生变化时，将最新的值设置在vue实例中，因为vue已经实现数据的响应化
 * 响应化的set函数会触发界面中所有函数依赖模块的更新，所以所有有该数据模块就都更新了
 */

//创建QVue类，接收一个options对象
class QVue{
	//构造方法
	constructor(options){
		//缓存option对象数据, $是为了防止命名污染
		this.$options = options;
		
		//取出data数据做数据响应
		this.$data = options.data||{};
		//短路或，避免$data为undefined*/
		
		//监听数据的变化,搭建observer与watcher的桥梁
		this.observe(this.$data);
		
		//解析vue语法对应的指令,并渲染到页面上,搭建compile与watcher的桥梁
			//解析el对应id的容器中的文档树(文本节点{{}},属性节点v-xx)
		new Compile(this,options.el);
		//此处this已经绑定好数据监听(app的set/get,即data的set/get)
	}
	
	//观察数据变化
	observe(data){
		//$data不存在或不是对象类型则不予监听,{}[]是object
		if( !data || typeof data !== "object"){
			return;
		}
		
		//取data中所有的属性名                                                (数组的话取出下标)   ["name","age"]
		let keys = Object.keys(data);
		//keys：Object内置对象方法，返回对象自身的属性组成的数组
		
		//循环data的每一个属性名   -->$data,app对象分别添加set get方法
		keys.forEach( (key)=>{
			//数据响应-defineProperty(data中的每个属性设置set/get)----(对象,属性名,属性值)
			this.defineReactive(data,key,data[key]);
			
			//代理data中的属性到vue实例-defineProperty(app的data中的每个属性设置set/get) 
			this.proxyData(key);
			//model中有的映射一份到view-model(view-model=Watcher桥梁)--双向绑定
		});
	}
	
	//数据响应defineProperty(给每个属性添加set/get方法，感知数据变化)
	defineReactive(data,key,val){
		//解决数据层次嵌套，递归绑定监听
		this.observe(val);
		//-->data中属性还是一个对象 data:{name:"name",addr:{pro:"湖南",city:"长沙"}}
		//若不循环，则监听的是addr对象，只有对象地址改变才监听得到，当值改变监听不到
		
		
		//管理watcher(请求和响应与谁)
		const dep= new Dep();
		
		
		//开始数据监听--data对象
		Object.defineProperty(data,key,{//第1次时,只是绑定set/get,并没有执行(不是因为值改变)
			get(){
				//update中new Watcher时会调用Watcher构造函数，构造函数中Dep.target=watcher的对象，然后会调用get函数就到了此处
				Dep.target && dep.addWatcher(Dep.target);
				//addWatcher会向watchers数组中保存target对象,也就是watcher对象
				//返回之后Watcher构造函数继续执行Dep.target=null
				
				//返回当前属性的值
				return val;
			},
			set(newVal){
				if(newVal===val){//值没有变
					return;
				}
				//值发生变化-->更新
				val=newVal;
				
				//值在更新,通知所有的watcher起作用,将页面中vue重新渲染一次
				dep.notify();
			}
		})
	}
	
	//代理data中的属性到vue实例上--app对象
		//model中有的映射一份到view-model(view-model=Watcher桥梁)--双向绑定
	proxyData(key){//key=data中的属性名,this=app
		Object.defineProperty(this,key,{
			get(){
				return this.$data[key];
				//$data中的所有属性都绑定过set/get,此处就相当于调用了$data中的get方法
			},
			set(newVal){
				this.$data[key] = newVal;
				//此处就相当于调用了$data中的set方法
			}
		})
	}
}

//解析vue语法对应的指令,并渲染到页面上,搭建compile与watcher的桥梁
class Compile{
	//vm:app对象 -- el:层的id名 -- this:Compile对象
	constructor(vm,el) {
	    this.$vm=vm;
	    this.$el=document.querySelector(el);
	    
	    //此处忽略<template>-->解决：加个else,el和template按两套标准解析
	    if(this.$el){
	    	//解析节点内容,将宿主元素的代码片段取出
	    	this.$fragment = this.nodeFragment(this.$el);
	    	//将vue语法对应的内容渲染上$fragment代码片段
	    	this.compile(this.$fragment);
	    	//将$fragment渲染到页面上
	    	this.$el.appendChild(this.$fragment);
	    }
	}
	
	//解析节点内容,将宿主元素的代码片段取出
	nodeFragment(el){
		//创建根节点
		const frag = document.createDocumentFragment();
		
		let child;
		while(child = el.firstChild){//直到取出的节点为undefined或null
			frag.appendChild(child);
		}
		return frag;
	}
	
	//分析容器内的vue语法，渲染$fragment代码片段
	compile(el){
		//取宿主节点下所有的子元素
		const childNodes = el.childNodes;
		
		//转成数组,迭代每个子元素
		Array.from(childNodes).forEach((childNode)=>{
			//判断是不是元素节点(标签)
			if(this.isElement(childNode)){
				console.log("编译元素节点的name:"+childNode.nodeName);
				//取出元素节点上所有的属性
				const nodeAttrs = childNode.attributes;
				
				//转成数组,迭代每个属性节点
				Array.from(nodeAttrs).forEach((nodeAttr)=>{
					//取属性名  --->  判断是普通属性(不需要操作)，还是vue语法中的属性(v-xxx/:/@)
					const attrName = nodeAttr.name;
					//取属性值  --->  如果不是普通属性，判断需要做的操作
					const attrValue = nodeAttr.value;
					
					//判断是不是指令  v-开头
					if(this.isDirective(attrName)){
						//取指令v-后面的内容
						const dir = attrName.substring(2);
						
						//执行更新   -->  不同的dir操作在compile中有对应的,以dir命名的函数来实现(text()/html()/on()..)
						this[dir] && this[dir](this.$vm,childNode,attrValue);
						//this[dir]:寻找compile中的dir属性  ---> 取到声明部分
						//&&:有对应的方法则继续
						//this[dir](this.$vm,childNode,attrValue):激活dir函数同时传入参数(vue实例(app),子元素,属性值)
					}
					
					//判断是不是事件处理   @
					if(this.isEvent(attrName)){
						//取出事件名  @click--click
						let dir = attrName.substring(1);
						
						//事件处理                        (vue实例(app)，子元素，属性值，事件类型)
						this.eventHandler(this.$vm,childNode,attrValue,dir);
					}
				})
			}else if(this.isInterPolation(childNode)){
				//判断是不是文本节点(内容是不是插值语法)
				
				//更新插值文本
				this.compileText(childNode);
				console.log("插值文本:"+childNode.textContent);
			}
			
			//递归子元素，解决元素嵌套问题   --> 有子节点且长度不为0
			if(childNode.childNodes && childNode.childNodes.length){
				this.compile(childNode);
			}
		})
	}
	
	//是否为元素节点
	isElement(node){
		return node.nodeType===1;
	}
	//是否为文本节点(内容是不是插值语法{{内容}})
	isInterPolation(node){
		return node.nodeType===3 && /\{\{(.*)\}\}/.test(node.textContent);
	}
	//是否为指令(v-xx)
	isDirective(attr){
		//indexOf:返回字符串第一次的索引
		return attr.indexOf("v-")==0;
	}
	//是否为事件(@xx)
	isEvent(attr){
		return attr.indexOf("@")==0;
	}
	
	//更新函数——桥接
	update(vm,node,exp,dir){
		//获取dir对应的函数名
		const updateFn = this[`${dir}Updater`];//``执行里面的占位符${dir}
		//函数不为空则激活函数     (标签对象，vue对象的属性值 ) 
		updateFn && updateFn(node,vm[exp]);//vm[exp]:在app对象中寻找exp属性
		
		
		//依赖收集     --> 桥梁——设置target,触发get,添加依赖
		new Watcher(vm,exp,function(value){
			updateFn && updateFn(node,value);
		})
	}
	
	//v-text
	text(vm,node,exp){
		this.update(vm,node,exp,"text");
	}
	textUpdater(node,value){
		//修改节点上文本内容
		node.textContent = value;
	}
	
	//v-model
	model(vm,node,exp){
		this.update(vm,node,exp,"model");
		
		//对input添加监听    当事件被触发时，调用回调函数
		node.addEventListener('input',(e)=>{
			vm[exp] = e.target.value;
		})
	}
	modelUpdater(node,value){
		node.value=value;
	}
	
	//v-html
	html(vm,node,exp){
		this.update(vm,node,exp,"html");
	}
	htmlUpdater(node,value){
		node.innerHTML=value;
	}
	
	//更新插值文本
	compileText(node){
		let key = RegExp.$1;
		this.update(this.$vm,node,key,"text");
	}
	
	//事件处理器  --> exp:回调函数名
	eventHandler(vm,node,exp,dir){
		//判断是否存在事件中调用的回调函数
		let fn = vm.$options.methods && vm.$options.methods[exp];
		if(dir && fn){
			//给事件添加监听
			node.addEventListener(dir,fn.bind(vm));
		}
	}
}

//桥梁——设置target,触发get,添加依赖
class Watcher{
	constructor(vm,key,func) {
		//vue实例
	    this.vm = vm;
	    //vue实例中需要更新的属性值
	    this.key=key;
	    //更新执行回调函数XXXUpdater
	    this.func=func;
	    
	    //给Dep新添加属性target(目标对象),target=当前Watcher实例,用于类间通信
	    Dep.target = this;//本来Dep原来没有target属性,在此处才加上的
	    
	    //触发get,添加依赖
	    this.vm[this.key];//app[name] --> 相当于调用对象app中get --> 相当于调用$data中get
	    
	    Dep.target=null;
	}
	
	//激活func回调函数---具体的更新操作
	update(){
		//通过回调函数更新页面      (vue实例,实例对应属性值)
		this.func.call(this.vm,this.vm[this.key]);
		//this --> watcher对象，每个对象的更新操作(即回调函数func的内容)可能不同
		//func --> updateFn && updateFn(node,value)调用对应类型的函数更新
	}
}


//管理Watcher的中间对象,用来存数据---方便A类存值,B类取值
class Dep{
	constructor() {
	    //页面中每个vue语法对应一个watcher
	    this.watchers=[];//初始化
	}
	
	//添加
	addWatcher(watcher){
		this.watchers.push(watcher);
	}
	
	//通知所有watcher更新
	notify(){
		this.watchers.forEach((watcher)=>{
			//通知更新
			watcher.update();
		})
	}
}